package bman.tools.net;

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.logging.Logger;

/**
 * This class is used to send and receive messages through sockets asynchronously.
 * <p>
 * The Radio keeps track of sockets that has already been created. If you send a message to 
 * a particular address, the Radio instance will reuse that socket for the next messages you 
 * send to that address. If an IOException sending to the socket, the socket will be discarded.
 * <p>
 * The recipient of the message can reply through the socket created when this
 * send method was invoked. The recipients reply will be received by the Receiver
 * associated with the Radio instance.
 * <p>
 * A Receiver implementation is needed for the Radio. The Receiver's receive() method will be invoked each
 * time the Radio receives a message. The Receiver's job is to interpret the message and perform the
 * necessary actions on it.  
 * 
 * @author MrJacky
 *
 */
public class Radio
{
	Logger log = Logger.getLogger(this.getClass().getName());
	
	ServerSocketListener socketListener;
	Receiver Receiver;
	
	Map<String, Socket> socketMap = new HashMap<String, Socket>();
	
	public Radio(Receiver r)
	{
		Receiver = r;
		
		int port = 778;
		int max = port +10;
		while(socketListener == null && port < max)
		{
			log.info("Attempting to create Radio with port "+port);
			try
			{
				socketListener = new ServerSocketListener(port++,r,this);
			}
			catch (Exception e)
			{
			}
		}
	}
	
	public Radio(Receiver r, int recievePort) throws Exception
	{
		Receiver = r;
		
		log.fine("Creating ServerSocketListener...");
		socketListener = new ServerSocketListener(recievePort,r,this);
		
		log.info("Radio Ready.");
	}
	
	/**
	 * This method was added so that ServerSocketListener can
	 * add new socket connections to the socketMap of Radio
	 */
	protected void addSocket(Socket s)
	{
		socketMap.put(s.toString(),s);
	}
	
	/**
	 * Sends the message to all Radio instance that has connected to this Radio
	 * The message format is <message_length>+<message>.
	 */
	public void broadcast(byte[] message)
	{
		Set<String> set = socketMap.keySet();
		Iterator<String> i = set.iterator();
		
		while(i.hasNext())
		{
			String key = i.next();
			send(key,message);
		}
	}
	
	/**
	 * Sends a message to another Radio instance.
	 * 
	 * Sends a message through a socket. The message format is <message_length>+<message>.
	 * <p>
	 * The Radio keeps track of sockets that has already been created. If you send a message to 
	 * a particular address, the Radio instance will reuse that socket for the next messages you 
	 * send to that address. If an IOException sending to the socket, the socket will be discarded.
	 * <p>
	 * The recipient of the message can reply through the socket created when this
	 * send method was invoked. The recipients reply will be received by the Receiver
	 * associated with the Radio instance.
	 * 
	 * these connections.
	 *  
	 * @param address - the IP:port or hostname:port where another Radio instance is active.
	 *                - If no port is provided the default port 777 is used.
	 *                - <br>Example values are: <br>
	 *                - <li>127.0.0.1:8080</li>
	 *                - <li>localhost:8080</li>
	 *                - <li>localhost</li>
	 *                
	 * @param message - the message to be sent.
	 * @return true if the message was sent, false if there was an exception
	 * 
	 */
	public boolean send(String address, byte[] message)
	{
		try
		{
			Socket s = null;
			
			String ip = address;
			int port = 777;
			
			int i = address.indexOf(":");
			if(i != -1)
			{
				ip = address.substring(0,i);
				
				if(i+1 < address.length())
					port = Integer.parseInt(address.substring(i+1));
			}

			s = socketMap.get(address);
			
			if(s == null)
			{
				s = new Socket(ip,port);
				new SocketListener(s,Receiver);
				socketMap.put(address,s);
			}
			
			boolean b =  send(s,message);
			if(b == false)
			{
				discard(s,address);
			}
			
			return b;
		}
		catch (Exception e)
		{
			log.warning("Unexpected error occured during send: "+e.toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}
	
	private void discard(Socket s, String key)
	{
		socketMap.remove(key);
		
		try
		{
			s.close();
			log.info("Socket has been discarded.");
		}
		catch(Exception e2)
		{
			//we dont need to see this.
		}
	}
	
	/**
	 * Sends a message through the socket using the standard Radio protocol.
	 * The message format is <message_length>+<message>.
	 */
	private boolean send(Socket s, byte[] message)
	{
		try
		{
			BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
			DataOutputStream dout = new DataOutputStream(bos);
			dout.writeInt(message.length);
			dout.write(message);
			dout.flush();
			return true;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			
			log.warning("Error in sending: "+ e);
			return false;
		}
	}
	
	/**
	 * Sends a message through the socket using the standard Radio protocol.
	 * The message format is <message_length>+<message>.
	 */
	public boolean reply(Socket s, byte[] message)
	{
		String key = s.toString();
		if(socketMap.containsKey(key) == false)
		{
			log.info("Adding socket to socketMap: "+key);
			socketMap.put(s.toString(),s);
		}
		
		boolean b = send(s,message);
		
		if(b == false)
		{
			discard(s,key);
		}
		
		return b;
	}
}


